<!DOCTYPE html>
<html>

<head>
  <title>canvas test-23 - canvas碰撞检测（光线投射法）</title>
  <style type="text/css">
  * {
    margin: 0;
    padding: 0
  }
  
  pre {
    padding: 10px 0
  }
  </style>
</head>

<body>
  <pre style="font-size: 14px">
    canvas碰撞检测（光线投射法）原理

      两物体A、B, 该方法假设碰撞时物体A运动，物体B不动，如果满足A速度方向直线与B物体顶部水平直线交点落在物体边界之间并且物体A在B的下方，则认为物体A、B碰撞

      转化为数学模型为求两条直线的交点(x,y)有：

      y = k1 * x + b1; // @1
      y = k2 * x + b2; // @2
      x >= B.x && x <= B.x + B.width && y >= B.y && y <= B.y + B.height;

      联立方程1,2解得：

      x = (b2 - b1) / (k1 - k2);
      y = (k1 * b2 - k2 * b1) / (k1 - k2);

    使用场景

      只适用于物体从上方掉落的情况，如球是否完全掉入桶中
  </pre>
  <p></p>
  <canvas id="stage" width="400" height="400" style="border: 1px solid red; margin: 20px"></canvas>
  <script type="text/javascript">
  var canvas = document.getElementById('stage');
  var ctx = canvas.getContext('2d');

  var objects = {};
  var bbox = canvas.getBoundingClientRect();
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var zindex = 1;
  var intersectionPoint = null; // 交点
  var helpAxes = null; // 水平辅助线
  var velocityLine = null; // 速度直线
  var circle = new KCircle(rnd(50, canvasWidth - 50), rnd(50, canvasHeight - 50), 20, 'rgb(0,255,0)', 'rgba(255,0,0,.5)');
  var rect1 = new KRect(rnd(10, canvasWidth - 90), rnd(10, canvasHeight - 90), 80, 80, 'rgb(0,120,0)', 'rgba(255,0,0,.5)');
  var rect2 = new KRect(rnd(10, canvasWidth - 90), rnd(10, canvasHeight - 90), 80, 80, 'rgb(0,120,0)', 'rgba(255,0,0,.5)');
  objects[circle.zindex] = circle;
  // objects[rect1.zindex] = rect1;
  objects[rect2.zindex] = rect2;

  window.onload = function() {
    renderAll();
  };

  function renderAll() {
    ctx.clearRect(0, 0, canvasWidth + 1, canvasHeight + 1);
    for (var i in objects) {
      objects[i].draw(ctx);
    }
    if (intersectionPoint) {
      ctx.beginPath();
      ctx.save();
      ctx.arc(intersectionPoint[0], intersectionPoint[1], 5, 0, Math.PI * 2);
      ctx.closePath();
      ctx.fillStyle = '#000';
      ctx.fill();
      ctx.restore();
      intersectionPoint = null;
    }
    if (helpAxes) {
      ctx.beginPath();
      ctx.save();
      ctx.moveTo(0, helpAxes[1]);
      ctx.lineTo(canvasWidth, helpAxes[1]);
      ctx.strokeStyle = 'green';
      ctx.closePath();
      ctx.stroke();
      ctx.restore();
      helpAxes = null;
    }
    if (velocityLine) {
      ctx.save();
      ctx.strokeStyle = 'red';
      ctx.beginPath();
      ctx.moveTo(velocityLine[0], velocityLine[1]);
      ctx.lineTo(velocityLine[2], velocityLine[3]);
      ctx.stroke();
      ctx.restore();
      velocityLine = null;
    }
  }

  // handlers

  var startX = 0;
  var startY = 0;
  var activeObject = null;
  var startHandler = null;
  var moveHandler = null;
  var endHandler = null;

  startHandler = bind(canvas, 'mousedown', onDragStart);

  function onDragStart(e) {
    var object = findActiveObject(e);
    var shouldRender = false;
    if (activeObject && activeObject !== object) {
      activeObject.setSelected(false);
      shouldRender = true;
    }

    startX = e.clientX;
    startY = e.clientY;
    activeObject = object;

    if (activeObject) {
      activeObject.ox = activeObject.x;
      activeObject.oy = activeObject.y;
      activeObject.setSelected(true);
      bringTop(activeObject);
      canvas.style.cursor = 'pointer';
      moveHandler = bind(document, 'mousemove', onDragMove);
      endHandler = bind(document, 'mouseup', onDragEnd);
    }

    if (shouldRender || activeObject) {
      renderAll();
    }
  }

  function onDragMove(e) {
    var dx = e.clientX - startX;
    var dy = e.clientY - startY;
    var cx = activeObject.x;
    var cy = activeObject.y;
    activeObject.x = activeObject.ox + dx;
    activeObject.y = activeObject.oy + dy;
    canvas.style.cursor = 'move';
    var kx = activeObject.x - cx + 0.000001;
    var ky = activeObject.y - cy;
    rayHitTest(activeObject, ky / kx, activeObject.x, activeObject.y);
    renderAll();
  }

  function onDragEnd() {
    moveHandler && moveHandler.remove();
    endHandler && endHandler.remove();
    canvas.style.cursor = 'default';
    if (activeObject) {
      activeObject.setSelected(false);
      activeObject = null;
      renderAll();
    }
  }

  function bringTop(object) {
    delete objects[object.zindex];
    object.zindex = zindex++;
    objects[object.zindex] = object;
  }

  function findActiveObject(e) {
    var p = getPointer(e.clientX, e.clientY);
    var object = null;
    for (var i in objects) {
      if (objects[i].containsPoint(p.x, p.y)) {
        if (!object || objects[i].zindex >= object.zindex) {
          object = objects[i];
        }
      }
    }
    return object;
  }

  function rayHitTest(obj, k1, x, y) {
    var b1 = y - k1 * x;
    var bounds1 = obj.getBounds();
    for (var key in objects) {
      var cur = objects[key];
      if (cur !== obj) {
        var bounds2 = cur.getBounds();
        var k2 = 0;
        var b2 = bounds2.top - k2 * bounds2.left;
        var kx = (b2 - b1) / (k1 - k2);
        // var ky1 = (k1 * b2 - k2 * b1) / (k1 - k2);
        var ky = k1 * kx + b1;
        helpAxes = [0, bounds2.top];
        velocityLine = [-b1 / k1, 0, (canvasHeight - b1) / k1, canvasHeight];
        if (isFinite(kx) && isFinite(ky)) {
          intersectionPoint = [kx, ky];
          if (
            kx > bounds2.left && kx < bounds2.right &&
            bounds1.top > bounds2.top && bounds1.top < bounds2.bottom
          ) {
            alert('fail in');
          }
        }
      }
    }
  }

  // helpers

  function rnd(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
  }

  function bind(elem, type, listener) {
    elem.addEventListener(type, listener, false);
    return {
      remove: function() {
        elem.removeEventListener(type, listener, false);
      }
    };
  }

  function getPointer(x, y) {
    var xr = canvasWidth / bbox.width;
    var yr = canvasHeight / bbox.height;
    return {
      x: (x - bbox.left) * xr,
      y: (y - bbox.top) * yr
    };
  }

  function KCircle(x, y, r, fillStyle, selectedStyle) {
    this.x = x;
    this.y = y;
    this.ox = x;
    this.oy = y;
    this.zindex = zindex++;
    this.raidus = r;
    this.fillStyle = fillStyle;
    this.selectedStyle = selectedStyle;
    this.selectd = false;
  }

  KCircle.prototype.type = 'circle';

  KCircle.prototype.setSelected = function(selected) {
    if (!this.fillStyle_) {
      this.fillStyle_ = this.fillStyle;
    }
    if (selected) {
      this.fillStyle = this.selectedStyle;
    } else {
      this.fillStyle = this.fillStyle_;
    }
    return this;
  };

  KCircle.prototype.draw = function(ctx) {
    ctx.save();
    ctx.fillStyle = this.fillStyle;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.raidus, 0, Math.PI * 2);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  };

  KCircle.prototype.getBounds = function() {
    return {
      left: this.x - this.raidus,
      right: this.x + this.raidus,
      top: this.y - this.raidus,
      bottom: this.y + this.raidus
    }
  };

  KCircle.prototype.containsPoint = function(x, y) {
    return Math.sqrt((this.x - x) * (this.x - x) + (this.y - y) * (this.y - y)) < this.raidus;
  };

  function KRect(x, y, width, height, fillStyle, selectedStyle) {
    this.x = x;
    this.y = y;
    this.ox = x;
    this.oy = y;
    this.zindex = zindex++;
    this.width = width;
    this.height = height;
    this.fillStyle = fillStyle;
    this.selectedStyle = selectedStyle;
    this.selectd = false;
  }

  KRect.prototype.type = 'rect';

  KRect.prototype.setSelected = function(selected) {
    if (!this.fillStyle_) {
      this.fillStyle_ = this.fillStyle;
    }
    if (selected) {
      this.fillStyle = this.selectedStyle;
    } else {
      this.fillStyle = this.fillStyle_;
    }
    return this;
  };

  KRect.prototype.draw = function(ctx) {
    ctx.save();
    ctx.fillStyle = this.fillStyle;
    ctx.beginPath();
    ctx.rect(this.x, this.y, this.width, this.height);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  };

  KRect.prototype.getBounds = function() {
    return {
      left: this.x,
      right: this.x + this.width,
      top: this.y,
      bottom: this.y + this.height
    }
  };

  KRect.prototype.containsPoint = function(x, y) {
    return x >= this.x && y >= this.y && x <= (this.x + this.width) && y <= (this.y + this.height);
  };

  function KText(text, x, y, font, color) {
    this.text = text;
    this.x = x;
    this.y = y;
    this.color = color;
    this.font = font || '14px Arial';
  }

  KText.prototype.setText = function(text) {
    this.text = text;
    return this;
  };

  KText.prototype.draw = function(ctx) {
    ctx.save();
    ctx.font = this.font;
    ctx.fillStyle = this.color;
    var geom = ctx.measureText(this.text);
    ctx.fillText(this.text, this.x - geom.width / 2, this.y - geom.height / 2);
    ctx.restore();
  };

  function transform(p, deg, dx, dy) {
    dx = dx || 0;
    dy = dy || 0;
    var cosa = Math.cos(deg);
    var sina = Math.sin(deg);
    // 数学上的直角坐标系中坐标变换矩阵是逆时针旋转deg度
    var matrix = [
      [cosa, -sina, dx],
      [sina, cosa, dy],
      [0, 0, 1]
    ];
    // canvas坐标系中的变化矩阵是顺时针旋转deg度
    // 由 cos(-a) = cosa; sin(-a) = -sina 知
    matrix[0][1] *= -1;
    matrix[1][0] *= -1;
    // 先平移还原到原始坐标系
    p[0] -= dx;
    p[1] -= dy;
    return [
      matrix[0][0] * p[0] + matrix[0][1] * p[1],
      matrix[1][0] * p[0] + matrix[1][1] * p[1]
    ];
  }
  </script>
</body>

</html>
